from PySide6.QtWidgets import QHeaderView, QMainWindow, QFileDialog, QTableWidgetItem,QDialog
from PySide6 import QtGui
from PySide6.QtCore import Qt, QTimer

from ui.datalogger_ui import Ui_DataCurve
from ui.dialog_ui import Ui_DownloadDialog

import serial
import serial.tools.list_ports
from datetime import datetime
import binascii
import struct
import threading
import time
import csv 
import os


# 定义数据帧类型
START_FRAME = 0x01
DATA_FRAME = 0x02
END_FRAME = 0x03

FRAME_HEADER = b'\xAA\x55'
FRAME_TAIL = b'\xDF\xAF'
HEADER_LEN = 2

class Window(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_DataCurve()
        self.dialog = QDialog()
        self.dl_dialog = Ui_DownloadDialog()
        self.dl_dialog.setupUi(self.dialog)
        self.ui.setupUi(self)

        # 表格初始化
        self.last_len= 0
        self.table_init()
        self.dialog_table_init()
        self.ui.label_DeviceTime.setText("设备未连接")

        # 设备连接和时间相关变量
        self.device_connected = False   # 设备连接标志
        self.device_time = None  # 存储设备时间
        self.device_start_time = None  # 设备时间的起始时间
        self.local_start_time = None   # 对应的本地时间起始点 

        # 传感器数据初始化 
        self.power_voltage = 0.0
        self.water_content1 = 0.0
        self.water_content2 = 0.0
        self.temperature1 = 0.0
        self.temperature2 = 0.0

         # 按钮初始化 -刷新按钮
        ico = QtGui.QPixmap("F:/myPro/qtproj/MyQt/HD_pyside/ico/reload.png")
        QtGui.QIcon().addPixmap(ico, QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On)
        self.ui.btnRefresh.setIcon(QtGui.QIcon(ico))
        # 断开按钮
        ico1 = QtGui.QPixmap("F:/myPro/qtproj/MyQt/HD_pyside/ico/com_disconnect.png")
        QtGui.QIcon().addPixmap(ico1, QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On)
        self.ui.btnOpenPort.setIcon(QtGui.QIcon(ico1))
        # 连接按钮
        ico2 = QtGui.QPixmap("F:/myPro/qtproj/MyQt/HD_pyside/ico/play.png")
        QtGui.QIcon().addPixmap(ico2, QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On)
        self.ui.startbtn.setIcon(QtGui.QIcon(ico2))
        #断开按钮
        ico2 = QtGui.QPixmap("F:/myPro/qtproj/MyQt/HD_pyside/ico/pause.png")
        QtGui.QIcon().addPixmap(ico2, QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On)
        self.ui.pausebtn.setIcon(QtGui.QIcon(ico2))
        # 开始采集按钮
        ico3 = QtGui.QPixmap("F:/myPro/qtproj/MyQt/HD_pyside/ico/save.png")
        QtGui.QIcon().addPixmap(ico3, QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On)
        self.ui.buttonLogging.setIcon(QtGui.QIcon(ico3))
        # 校准时间按钮
        ico4 = QtGui.QPixmap("F:/myPro/qtproj/MyQt/HD_pyside/ico/time.png")
        QtGui.QIcon().addPixmap(ico4, QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On)
        self.ui.btnCaliTime.setIcon(QtGui.QIcon(ico4))

    # 采集间隔
        self.ui.comboBox.addItems(["1s","2s","4s","5s","10s","15s","20s","30s",\
                                    "1m","5m","10m","15m","20m","30m","1h","1h30m",\
                                    "2h","3h"])
        # 添加comboBox变化事件监听器
        self.ui.comboBox.currentTextChanged.connect(self.on_combobox_changed)

        # 串口初始化
        self.frames =[]   #接收数据缓冲
        self.port_is_open = False 
        self.serial_port = None
        self.is_receiving = False  
        self.receive_thread = None
        self.download_flag = False    #  下载控件标志
        self.get_file_flag = False # 获取文件标志
        self.data_buffer = ""  # 用于缓存不完整的数据
        self.click_row = None


        self.file_buffer = bytearray()
        self.files_list = []                # TF卡文件列表
        self.recv_buffer = bytearray()       # 缓存接收的字节，防止断帧
        self.content_list = []        # 数据内容
        self.expected_lines = 0      # 预期接收数据帧数
        self.received_lines = 0      # 实际接收数据帧数
        self.received_seq_set = set()   #存储已接收序号避免重复
        self.download_file_name = None
  
        # 数据帧初始化
        self.cmd_head = ''         # 帧头
        self.cmd_type = ''         # 命令类型(UPDATE_CONTROL)
        self.ctrl_msg = ''         # CtrlMsgType-指示消息的类型
        self.screen_id = ''        # 产生消息的画面ID
        self.control_id = ''       # 产生消息的控件ID
        self.control_type = ''     # 控件类型
        self.param = ''            # 可变长度参数
        self.cmd_tail = ''         # 帧尾
        
        # 按钮信号初始化
        self.ui.btnRefresh.clicked.connect(self.comRefresh)
        self.ui.btnOpenPort.clicked.connect(self.serial_open)
        self.ui.startbtn.clicked.connect(self.connect_logger)
        self.ui.pausebtn.clicked.connect(self.disconnect_logger)
        self.ui.buttonLogging.clicked.connect(self.set_intervalTime)     # 采集间隔按钮
        self.ui.btnCaliTime.clicked.connect(self.set_currentTime)        # 获取时间按钮
        self.ui.btnDownload.clicked.connect(self.download_data)          # 下载按钮
        self.ui.pushButton.clicked.connect(self.choose_file)             
        self.ui.realbtn.clicked.connect(self.real_data)
        # dialog按钮绑定
        self.dl_dialog.buttonBox.accepted.connect(self.download_confirm)
        self.dl_dialog.buttonBox.rejected.connect(self.download_cancel)
        self.dialog.finished.connect(self.dialog_closed)
        
        # 设置数据下载默认路径
        self.script_dir = os.path.join(os.path.expanduser("~"), "Desktop")
        self.ui.lineEdit.setText(self.script_dir)
        self.ui.lineEdit.setReadOnly(True)
    
        # 绑定Dialog信号，单元格点击事件
        self.dl_dialog.tableWidget.cellPressed.connect(self.on_cell_clicked)

        #实时显示当前时间
        self.current_time = datetime.now()
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update_local_time)
        self.timer.start(1000)  # 每秒钟更新一次时间
        self.update_local_time()

    def update_local_time(self):
        #本地时间更新
        self.current_time = datetime.now()
        show_time =self.current_time.strftime("%Y-%m-%d %a %H:%M:%S")
        self.ui.label_localTime.setText(show_time)
        #设备时间更新
        if self.device_connected and self.device_start_time and self.local_start_time:
        # 计算时间差
            time_elapsed = self.current_time - self.local_start_time
            current_device_time = self.device_start_time + time_elapsed
            device_show_time = current_device_time.strftime("%Y-%m-%d %H:%M:%S")
            self.ui.label_DeviceTime.setText(device_show_time)

    # 对比设备时间格式      
    def is_datetime_format(self, text):
        '''
        检查文本是否为日期时间格式
        支持多种常见格式：YYYY-MM-DD HH:MM:SS 或 YYYY/MM/DD HH:MM:SS
        '''
        import re
        # 匹配多种日期时间格式
        patterns = [
            r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$',  # 2024-01-01 12:30:45
            r'^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}$',  # 2024/01/01 12:30:45
            r'^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}$',  # 01-01-2024 12:30:45
            r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$'   # 2024-01-01T12:30:45
        ]
        for pattern in patterns:
            if re.match(pattern, text):
                return True
        return False

    def stop_device_time_update(self):
        '''
        停止设备时间更新（断开连接时调用）
        '''
        self.send_serial_data(self.serial_port, cmd_type='e7')
        self.device_connected = False
        self.device_start_time = None
        self.local_start_time = None
        self.ui.label_DeviceTime.setText("设备未连接")
        self.ui.tableWidget.clearContents()

     # 刷新串口
    def comRefresh(self):
        # 清空下拉列表内容
        self.ui.cbbPortNum.clear()
        # 列出当前可用串口
        ports = serial.tools.list_ports.comports()
        available_ports = []
        for port, desc, hwid in sorted(ports):
            # print(f"{port}: {desc} ")
            available_ports.append(port)
        self.ui.cbbPortNum.addItems(available_ports)

    # 打开串口
    def serial_open(self):
        # 检查串口是否没打开，如果没开，那么就打开并改变开关状态和图标
        if not self.port_is_open:
            port = self.ui.cbbPortNum.currentText()
            self.port_is_open = True
            try:
                self.serial_port = serial.Serial(port=port,baudrate=9600,timeout=5) # 该代码会阻塞，直到串口打开
                ico1 = QtGui.QPixmap("F:/myPro/qtproj/MyQt/HD1_LOG_QT/Qt_HD_Project/ico/com_connect_1.png")
                QtGui.QIcon().addPixmap(ico1, QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
                self.ui.btnOpenPort.setIcon(QtGui.QIcon(ico1))
                self.ui.btnOpenPort.setText("关闭串口")
                # self.serial_port.open()      # 无需再次打开串口
            except Exception as e:
                print(f"串口打开失败：{e}")
                self.port_is_open = False  # 确保标志正确
                return False
        else:
            # 如果串口已经打开，那么就关闭串口和改变按钮状态和图标
            try:
                # 先断开设备连接（如果已连接）
                if self.device_connected or self.is_receiving:
                    self.disconnect_logger()
                # 关闭串口
                if self.serial_port and self.serial_port.is_open:
                    self.serial_port.close()
                    print("串口已关闭")
                
                self.port_is_open=False
                ico1 = QtGui.QPixmap("F:/myPro/qtproj/MyQt/HD1_LOG_QT/Qt_HD_Project/ico/com_disconnect.png")
                QtGui.QIcon().addPixmap(ico1, QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On)
                self.ui.btnOpenPort.setIcon(QtGui.QIcon(ico1))
                self.ui.btnOpenPort.setText("打开串口")

            except Exception as e:
                 print(f"串口关闭失败：{e}")
                 self.port_is_open = False  # 即使关闭失败也要重置标志

    # 数据表格初始化
    def table_init(self):
        self.ui.tableWidget.setColumnCount(3)
        self.ui.tableWidget.setRowCount(13)
        self.ui.tableWidget.setHorizontalHeaderLabels(['ID','参数名称','实际值'])
        self.ui.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
        # 设置水平表头居中
        self.ui.tableWidget.horizontalHeader().setDefaultAlignment(Qt.AlignmentFlag.AlignCenter)
        # 设置垂直表头居中
        self.ui.tableWidget.verticalHeader().setDefaultAlignment(Qt.AlignmentFlag.AlignCenter)
        self.ui.tableWidget.setAlternatingRowColors(True)

    # dilog表头初始化
    def dialog_table_init(self):
        # 下载对话框初始化
        self.dl_dialog.tableWidget.setColumnCount(4)
        self.dl_dialog.tableWidget.setHorizontalHeaderLabels(["下载数据","文件名称","下载进度","删除数据"])
        # self.dl_dialog.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) # 自动适应串口行宽和行高
        # 设置水平表头居中
        self.dl_dialog.tableWidget.horizontalHeader().setDefaultAlignment(Qt.AlignmentFlag.AlignCenter)
        # 设置垂直表头居中
        self.dl_dialog.tableWidget.verticalHeader().setDefaultAlignment(Qt.AlignmentFlag.AlignCenter)
        self.dl_dialog.tableWidget.setAlternatingRowColors(True)

    # 填充数据到数据表格
    def table_fill(self,id,water,water_temp,votage):
        try:
            row_index = (id - 1)*2  # 转换为表格行索引 (0-5)
            i= 1
            # 创建表格项并设置数据
            id_item = QTableWidgetItem('No.'+str(id))
            water_item = QTableWidgetItem(f"{water:.2f}")
            # 设置文本居中对齐
            # id_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
            # water_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
            # temp_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
            # power_votage.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
            # 第一行水分
            self.ui.tableWidget.setItem(row_index, 0, id_item)           # ID列
            self.ui.tableWidget.setItem(row_index, 2, water_item)        # 水分含量列
            # 第二行温度
            id_item = QTableWidgetItem('No.'+str(id))
            temp_item = QTableWidgetItem(f"{water_temp:.2f}")
            self.ui.tableWidget.setItem(row_index+i, 2, temp_item)         # 温度列  
            # 第三行
            power_votage = QTableWidgetItem(f"{votage:.3f}")
            votage_item = QTableWidgetItem("power votage")
            i= i+1
            self.ui.tableWidget.setItem(row_index+ i, 0, votage_item)       # ID列
            self.ui.tableWidget.setItem(row_index+ i, 2, power_votage)      # 供电 
        except Exception as e:
            print(f"表格填充错误: {e}")

    def choose_file(self):
        self.script_dir = QFileDialog.getExistingDirectory(self, "选择文件夹")
        if self.script_dir:
            print('获取文件路径',self.script_dir)
            self.ui.lineEdit.setText(self.script_dir)
            self.ui.lineEdit.setReadOnly(True)
        

    def closeEvent(self, event):
        """重写窗口关闭事件，确保在关闭时断开与设备的连接"""
        if self.port_is_open:
            self.disconnect_logger()
        event.accept() # 接受关闭事件

    # 连接数采
    def connect_logger(self):
        try:
            # 检查是否有选择串口
            if not self.ui.cbbPortNum.currentText():
                print("请先选择串口")
                return False
            # 检查串口是否已打开
            if not self.serial_port or not self.serial_port.is_open:
                    print("串口未打开，请先打开串口")
                    return False
            # 清除串口缓冲区（连接前清理）
            try:
                self.serial_port.reset_input_buffer()
                self.serial_port.reset_output_buffer()
                # 初始化应用层缓冲区
                self.data_buffer = ""
                self.file_buffer = bytearray()
                if hasattr(self, 'recv_buffer'):
                    self.recv_buffer.clear()
                print("已清理缓冲区，准备连接")
            except Exception as e:
                print(f"清理缓冲区失败：{e}")

            try:
                # 尝试获取串口状态，如果失败则说明串口有问题
                self.serial_port.in_waiting
            except Exception as e:
                print(f"串口状态异常：{e}")
                # 关闭有问题的串口
                try:
                    if self.serial_port and self.serial_port.is_open:
                        self.serial_port.close()
                        self.port_is_open = False
                        # 更新串口按钮状态
                        ico1 = QtGui.QPixmap("F:/myPro/qtproj/MyQt/HD1_LOG_QT/Qt_HD_Project/ico/com_disconnect.png")
                        QtGui.QIcon().addPixmap(ico1, QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On)
                        self.ui.btnOpenPort.setIcon(QtGui.QIcon(ico1))
                        self.ui.btnOpenPort.setText("打开串口")
                except:
                    pass
                return False
                # 如果没有正在接收数据，则启动接收
            if not self.is_receiving:
                # 先启动数据接收线程
                self.receive_data_thread()
                # 发送设备时间查询指令
                self.send_serial_data(self.serial_port, cmd_type='dd')
                # 设备连接成功，更新按钮状态
                self.ui.startbtn.setEnabled(False)
                self.ui.pausebtn.setEnabled(True)
                print("已发送设备连接查询指令")
            else:
                print("数据接收进行中")   
        except Exception as e:
            print(f"连接设备失败：{e}")
            self.device_connected = False
            self.stop_receive_data_thread()
            return False

    # 停止连接数采
    def disconnect_logger(self):
        try:
             # 检查串口状态
            if not self.serial_port or not self.serial_port.is_open:
                # 如果串口未打开，则禁用暂停按钮，启用开始按钮
                self.ui.startbtn.setEnabled(True)
                self.ui.pausebtn.setEnabled(False)
                print("串口未打开，无法停止连接")
                return False
            # 如果正在接收数据，则停止接收
            if self.is_receiving:
                self.stop_receive_data_thread()
                print("已停止数据接收")
            # 重置设备连接状态
            self.device_connected = False
            self.download_flag = False
            self.stop_device_time_update()
            
            # 清除串口缓冲区数据
            try:
                if self.serial_port and self.serial_port.is_open:
                    # 清除输入缓冲区（接收缓冲区）
                    self.serial_port.reset_input_buffer()
                    # 清除输出缓冲区（发送缓冲区）
                    self.serial_port.reset_output_buffer()
                    print("已清除串口缓冲区数据")
            except Exception as buffer_error:
                print(f"清除串口缓冲区失败：{buffer_error}")

            # 清除应用层数据缓冲区
            try:
                self.data_buffer = ""  # 清空数据帧缓冲区
                self.file_buffer = bytearray()  # 清空文件缓冲区
                if hasattr(self, 'recv_buffer'):
                    self.recv_buffer.clear()  # 清空接收缓冲区
                print("已清除应用层数据缓冲区")
            except Exception as app_buffer_error:
                print(f"清除应用层缓冲区失败：{app_buffer_error}")

            # 更新按钮状态
            self.ui.startbtn.setEnabled(True)
            self.ui.pausebtn.setEnabled(False)
            
        except Exception as e:
            print(f"停止数据接收失败：{e}")
            self.ui.startbtn.setEnabled(True)
            self.ui.pausebtn.setEnabled(False)
            self.stop_device_time_update()
    # 时间文本转秒
    def parse_interval_time(self, interval_text):
        """
        解析时间间隔文本，返回对应的秒数
        支持格式："1s", "2m", "1h", "1h30m" 等
        """
        import re
        # 移除所有空格
        interval_text = interval_text.replace(" ", "")
        total_seconds = 0
        # 匹配小时部分 (如 "1h", "2h")
        hour_match = re.search(r'(\d+)h', interval_text)
        if hour_match:
            hours = int(hour_match.group(1))
            total_seconds += hours * 3600  # 1小时 = 3600秒
        # 匹配分钟部分 (如 "30m", "5m")
        minute_match = re.search(r'(\d+)m', interval_text)
        if minute_match:
            minutes = int(minute_match.group(1))
            total_seconds += minutes * 60  # 1分钟 = 60秒
        # 匹配秒部分 (如 "30s", "5s")
        second_match = re.search(r'(\d+)s', interval_text)
        if second_match:
            seconds = int(second_match.group(1))
            total_seconds += seconds
        
        return total_seconds

    # 秒转文本
    def seconds_to_interval_text(self, total_seconds):
        """
        将秒数转换回时间间隔文本格式
        例如：300秒 -> "5m", 3600秒 -> "1h"
        """
        # comboBox中的所有选项
        interval_options = ["1s","2s","4s","5s","10s","15s","20s","30s",
                           "1m","5m","10m","15m","20m","30m","1h","1h30m",
                           "2h","3h"]
        # 为每个选项计算对应的秒数，找到匹配的
        for option in interval_options:
            if self.parse_interval_time(option) == total_seconds:
                return option
        # 如果没有精确匹配，尝试转换为最接近的格式
        if total_seconds < 60:
            return f"{total_seconds}s"
        elif total_seconds < 3600:
            minutes = total_seconds // 60
            return f"{minutes}m"
        else:
            hours = total_seconds // 3600
            remaining_minutes = (total_seconds % 3600) // 60
            if remaining_minutes == 0:
                return f"{hours}h"
            else:
                return f"{hours}h{remaining_minutes}m"

    def set_intervalTime(self):
        # 检查串口状态
        if not self.device_connected:
            print("串口未打开，无法设置采集间隔")
            return False
        interval_text = self.ui.comboBox.currentText()
        interval_seconds = self.parse_interval_time(interval_text)
        # print(interval_seconds)
        if self.send_serial_data(self.serial_port,cmd_type='b111',control_type='11',value=interval_seconds):
            self.ui.buttonLogging.setText("开始采集OK")
            return True

    def download_data(self):
        self.dl_dialog.tableWidget.clearContents()         # 表格清空
        self.dl_dialog.tableWidget.setRowCount(0)
        self.dl_dialog.tableWidget.setColumnCount(0)
        self.dialog_table_init()
        self.file_buffer = bytearray()  # 缓存二进制数据清空
        self.files_list = []     # 文件列表清空
        self.last_len = 0   # dialog 上次接收数据长度
        self.download_flag = True
        self.get_file_flag = True
        self.send_serial_data(self.serial_port, cmd_type='e8')
        self.dialog.show()
        self.dialog.exec()

    def on_cell_clicked(self, row, column):
        if column == 0:
            # “下载”列被单元格点击了
            self.click_row = row
            self.download_file_name = None
            file_item = self.dl_dialog.tableWidget.item(row, 1)
            if file_item:
                self.download_file_name = file_item.text()
                # print(f"点击下载按钮，文件名：{self.download_file_name}")
                self.dl_dialog.tableWidget.setEnabled(False)  # 禁用项，使其不可点击/交互
                self.send_serial_data(self.serial_port,cmd_type='b111',control_type='11',
                            value=self.download_file_name, control_id='0013' )
                # self.get_file_flag=False  # 关闭文件接收条件
        if column ==3:
            # 删除列单元格
            dell_item = self.dl_dialog.tableWidget.item(row, 1)
            if dell_item:
                dell_file = dell_item.text()
                # print(f"点击删除按钮，文件名：{dell_file}")
                self.send_serial_data(self.serial_port,cmd_type='b111',control_type='11',
                                        value=dell_file, control_id='0014' )
                self.dl_dialog.tableWidget.removeRow(row)        # 表格删除行
        self.get_file_flag=False  # 关闭文件接收条件
        

    def download_confirm(self):
        self.download_flag = False

    def download_cancel(self):
        self.download_flag = False

    def dialog_closed(self):
        self.download_flag = False

    def real_data(self):
        self.send_serial_data(self.serial_port, cmd_type='e9')

    def on_combobox_changed(self):
        """当comboBox选项改变时，恢复按钮文本"""
        self.ui.buttonLogging.setText("开始采集")
        
    # 校准设备时间
    def set_currentTime(self):
        if self.device_connected:
            set_t = self.current_time.strftime("%Y-%m-%d %H:%M:%S")
            c_t = set_t.split(' ')
            c_t1 = c_t[0].split('-')
            c_t2 = c_t[1].split(':')
            year = int(c_t1[0])%100
            month = int(c_t1[1])
            week = self.current_time.weekday() + 1
            day = int(c_t1[2])
            hours = int(c_t2[0])
            minutes = int(c_t2[1])
            seconds = int(c_t2[2])
            def to_bcd(value):
                """将十进制数转换为BCD码的十六进制字符串"""
                tens = value // 10
                ones = value % 10
                return f"{tens:01x}{ones:01x}"
            year_bcd = to_bcd(year)
            month_bcd = to_bcd(month)
            week_bcd = to_bcd(week)
            day_bcd = to_bcd(day)
            hours_bcd = to_bcd(hours)
            minutes_bcd = to_bcd(minutes)
            seconds_bcd = to_bcd(seconds)
            text_value = (year_bcd + month_bcd + week_bcd + day_bcd +
                        hours_bcd + minutes_bcd + seconds_bcd)
            self.send_serial_data(self.serial_port,'f7',value=text_value)
            # 更新设备时间同步基准
            self.device_start_time = self.current_time  # 设置设备时间起始点为当前时间
            self.local_start_time = self.current_time   # 设置本地时间起始点为当前时间
            self.device_connected = True                # 确保设备连接状态为True
            self.ui.label_DeviceTime.setText(set_t)
        else:
            print("设备未连接")
            return False
    # 解析数据帧            
    def pare_cmd_buffer(self,dataframe):
        try:
            self.cmd_head = dataframe[:2]              # 帧头   半个字节
            self.cmd_type = dataframe[2:4]             # 命令类型(UPDATE_CONTROL)如，0xB1
            self.ctrl_msg = dataframe[4:6]             # 指示消息的类型,如，0x10/0x07接收mcu信息
            self.screen_id = dataframe[6:10]           # 产生消息的画面ID
            self.control_id = dataframe[10:14]           # 产生消息的控件ID  1bytes
            self.param = dataframe[14:-8]                # 可变长度参数
            self.cmd_tail = dataframe[-8:]               # 帧尾
        except Exception as e:
            print(f"帧解析异常: {e}, 帧数据: {dataframe}")
           
    # 信息处理流程
    def ProcessMessage(self):
        control_type = {
            '07' : self.NotifyIntText,  # 格式化的文本
            '10' : self.NotifyText,    # 文本数值
        }
        if self.cmd_type == "b1":
            # print(f"处理消息 - 命令类型: {self.cmd_type}, 控制消息: {self.ctrl_msg}")  
            handler = control_type.get(self.ctrl_msg)
            if handler:
                handler(self.screen_id,self.control_id,self.param)
            else:
                print(f"未知的控制类型: {self.ctrl_msg}")
        else:
            print(f"未处理的命令类型: {self.cmd_type}")  
    # 接收整型和浮点数据处理
    def NotifyIntText(self,screen_id, control_id,value):
        '''
          brief  按钮控件通知  
          details  当按钮状态改变，执行此函数
          param screen_id 画面ID
          param control_id 控件ID
          param state 按钮状态：0弹起，1按下
        '''
        try:
            screen_id = screen_id[-2:]
            control_id = control_id[-2:]
            # 检查数据长度是否足够
            if len(value) < 4:
                print(f"NotifyIntText 数据长度不足: {len(value)}, 数据: {value}")
                return
            if len(value) != 12:
               print(f"NotifyIntText 参数长度异常: 期望12个字符，实际{len(value)}个字符, 数据: {value}")
               return
            sign = value[:2] # 数据类型：00无符号、01有符号整型、02单精度浮点型、03双精度浮点型
            fill_zero = value[2:4] # 填充零：不足补0
            float_data = value[4:]
            # 检查浮点数据部分长度
            if len(float_data) != 8:  # 4字节 = 8个十六进制字符
                print(f"NotifyIntText 浮点数据长度错误: 期望8个字符，实际{len(float_data)}个字符, 数据: {float_data}")
                return
            try:
                float_num = struct.unpack('>f',bytes.fromhex(value[4:]))[0]
            except struct.error as e:
                print(f"NotifyIntText 浮点数解析错误: {e}, 数据: {float_data}")
                return
            except ValueError as e:
                print(f"NotifyIntText 十六进制转换错误: {e}, 数据: {float_data}")
                return
            if screen_id == '00' :
                if control_id == '03':
                    self.power_voltage = float_num  # 解析出的电压
            else:
                try:
                    sensor_id = int(screen_id) # 将screen_id转换为整数作为传感器ID
                    # 初始化或获取传感器的水分和温度数据
                    if not hasattr(self, f'water_content{sensor_id}'):
                        setattr(self, f'water_content{sensor_id}', 0.0)
                    if not hasattr(self, f'temperature{sensor_id}'):
                        setattr(self, f'temperature{sensor_id}', 0.0)

                    if control_id == '01':
                        setattr(self, f'water_content{sensor_id}', float_num)  # 解析出的水分含量
                    elif control_id == '02':
                        setattr(self, f'temperature{sensor_id}', float_num)  # 解析出的温度

                    # 调用table_fill，使用动态获取的属性值
                    self.table_fill(sensor_id, 
                                    getattr(self, f'water_content{sensor_id}'), 
                                    getattr(self, f'temperature{sensor_id}'), 
                                    self.power_voltage)  
                except ValueError:
                    print(f"NotifyIntText 无效的传感器ID: {screen_id}")
                    return                  
        except Exception as e:
            print(f"NotifyIntText 处理异常: {e}")
            return
    # 接收文本处理
    def NotifyText(self,screen_id, control_id,text):
        '''
          brief  文本控件通知  
          details  当文本通过屏幕按键更新，执行此函数
          param screen_id 画面ID
          param control_id 控件ID
          param  text     文本内容
        '''
        try:
            screen_id = screen_id[-2:]
            control_id = control_id[-2:]
    
            # 尝试多种解码方式
            ascii_text = None
            try:
                ascii_text = bytes.fromhex(text).decode('ascii')
            except UnicodeDecodeError:
                try:
                     #如果ASCII失败，尝试UTF-8解码
                    ascii_text = bytes.fromhex(text).decode('utf-8', errors='ignore')
                    print(f"使用UTF-8解码: {ascii_text}")
                except UnicodeDecodeError:
                    print('未解析出来')
                   
            except ValueError as e:
                print(f"十六进制转换错误: {e}, 数据: {text}")
                return
            if ascii_text is None:
                print(f"文本解码失败: {text}")
                return
             # 检查是否为日期时间格式
            if self.is_datetime_format(ascii_text):
                # 解析设备时间
                try:
                    self.device_start_time = datetime.strptime(ascii_text, "%Y-%m-%d %H:%M:%S")
                    self.local_start_time = datetime.now() 
                    # 显示设备时间
                    self.ui.label_DeviceTime.setText(ascii_text)
                    # 设置连接成功标志
                    self.device_connected = True
                    print(f'设备时间: {ascii_text}')
                    print('设备连接成功，开始同步时间')
                    return
                except ValueError as e:
                    print(f"时间格式解析错误: {e}")
                    self.device_connected = False
            # 尝试转换为浮点数
            try:
                text_value = float(ascii_text)
                if screen_id == '00' and control_id == '01':
                    # print('text:',ascii_text)
                    interval_text = self.seconds_to_interval_text(int(text_value))
                    index = self.ui.comboBox.findText(interval_text)
                    if index >= 0:
                        self.ui.comboBox.setCurrentIndex(index)
                        print(f"设备返回时间间隔: {text_value}秒 -> {interval_text}")
                    else:
                        print(f"未找到匹配的时间间隔选项: {interval_text}")

            except ValueError:
                # 处理非数字字符串
                if ascii_text == "Error":
                    self.device_connected = False
                    self.stop_device_time_update()
                    print("设备报告错误状态")
                        
        except Exception as e:
            print(f"NotifyText处理错误: {e}")
            self.device_connected = False
      
    # 开启线程接收数据
    def receive_data_thread(self): 
        if self.serial_port is not None and self.serial_port.is_open:  
            self.is_receiving = True
            if self.receive_thread is not None and self.receive_thread.is_alive():
                self.stop_receive_data_thread()
            self.receive_thread = threading.Thread(target=self.receive_data_loop)
            # self.receive_thread.daemon = True
            self.receive_thread.start()
        else:
            print("串口未打开，不能开启数据接收线程。")
            return False

    # 停止接收数据线程
    def stop_receive_data_thread(self):
        self.is_receiving = False
        if self.receive_thread is not None and self.receive_thread.is_alive():
            try:
                self.receive_thread.join(timeout=2.0)
                if self.receive_thread.is_alive():
                    print("警告：线程未能在超时时间内正常退出")
                else:
                    print("数据接收线程已成功停止。")
            except Exception as e:
                print(f"停止线程时发生错误：{e}")
            finally:
                self.receive_thread = None 
     #验证帧结构的有效性
    def validate_frame_structure(self, frame):
        """
        验证帧结构的有效性
        """
        try:
            # 检查帧头
            if not frame.startswith("ee"):
                return False  
            # 检查帧尾
            if not frame.endswith("fffcffff"):
                return False
            # 检查最小长度（帧头2 + 命令类型2 + 控制消息2 + 画面ID4 + 控件ID4 + 帧尾8 = 22）
            if len(frame) < 22:
                return False 
            # 提取命令类型
            cmd_type = frame[2:4]
            if cmd_type not in ['b1', 'f7', 'dd', 'e7', 'e8','e9']: 
                print(f"未知命令类型: {cmd_type}")
                return False
            # 提取控制消息类型
            ctrl_msg = frame[4:6]
            if ctrl_msg not in ['07', '10','11']:  # 只处理已知的控制消息类型
                print(f"未知控制消息类型: {ctrl_msg}")
                return False 
            return True
        except Exception as e:
            print(f"帧验证异常: {e}")
            return False
    
    # 数据接收线程循环
    def receive_data_loop(self): 
        while self.is_receiving :  
            try:
                if self.serial_port.in_waiting > 0:
                    rec_data = self.serial_port.read(self.serial_port.in_waiting)
                    if not self.download_flag :
                        frames = self.receive_frame_data(rec_data)  # 获取所有的数据帧
                        if frames:
                            for frame in frames:
                                # print(f"接收到数据帧: {frame}")
                                self.pare_cmd_buffer(frame)   # 解析每帧数据帧
                                self.ProcessMessage()         # 处理每帧数据帧
                    else:
                        if self.get_file_flag:     # 接收TF文件名
                            self.file_buffer.extend(rec_data)   # b'数据缓存
                            self._file_list_from_stream(self.file_buffer,self.files_list) # 转换为列表字符串文件
                            self.dialog_table_fill(self.files_list)
                        else:
                            # print("接收SCV数据")
                            self.feed_data(rec_data)
                else:
                    time.sleep(0.01)
            except Exception as e:
                print(f"数据接收线程错误：{e}")
                break
        print("数据接收线程已退出")

    # 解析数据文件
    def _file_list_from_stream(self,buffer, file_list):
        full_str = buffer.decode(errors='ignore')
        last_pos = 0
        while True:
            idx = full_str.find('.csv', last_pos)
            if idx == -1:
                break
            name_start = idx
            while name_start > 0 and (full_str[name_start-1].isdigit() or full_str[name_start-1] in '-_'):
                name_start -= 1
            filename = full_str[name_start:idx+4]
            if filename.endswith('.csv') and len(filename) > 8:
                file_list.append(filename)
            last_pos = idx + 4
        # 只保留未处理的部分
        remain_bytes = buffer[last_pos:]
        buffer.clear()
        buffer.extend(remain_bytes)

        # dialog数据填充
    def dialog_table_fill(self,filelist):
        # 下载对话框表格填充
        files_len = len(filelist)
        if files_len > self.last_len:
            for i in range(self.last_len,files_len):
                row = self.dl_dialog.tableWidget.rowCount()
                self.dl_dialog.tableWidget.insertRow(row)
                # 第一列 设置复选框
                download_cell = QTableWidgetItem("下载")
                # 第二列 设置文件名文本
                file_name = filelist[i]
                file_item = QTableWidgetItem(file_name)
                file_item.setFlags(file_item.flags() & ~Qt.ItemFlag.ItemIsEditable)  # 不可编辑
                # 第三列 下载进度列（初始为“0%”）
                progress_item = QTableWidgetItem("0%")
                progress_item.setFlags(progress_item.flags() & ~Qt.ItemFlag.ItemIsEditable)  # 禁止编辑
                # 第四列 删除列
                dele_item = QTableWidgetItem("删除")
                # 设置文本居中对齐
                download_cell.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
                file_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
                progress_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
                dele_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
                # 填充目录
                self.dl_dialog.tableWidget.setItem(row, 0, download_cell)
                self.dl_dialog.tableWidget.setItem(row, 1, file_item)
                self.dl_dialog.tableWidget.setItem(row, 2, progress_item)
                self.dl_dialog.tableWidget.setItem(row, 3, dele_item )
                self.dl_dialog.tableWidget.resizeColumnsToContents()
        self.last_len = len(filelist)
        
    # 解析一包数据
    def receive_frame_data(self,data):
        data_head = "ee"
        data_end = "fffcffff" 
        # 二进制字节串转16进制再解码为字符串
        datastr = binascii.hexlify(data).decode('utf-8')
        # print(f"接收到原始数据: {datastr}")  # 调试：打印原始数据
        self.data_buffer += datastr
        frames = []
        # 寻找所有的帧数据
        while True:
            # 寻找帧头
            head_index = self.data_buffer.find(data_head)
            if head_index == -1:
                break
             # 从帧头位置开始寻找帧尾
            search_start = head_index + len(data_head)  # 1
            end_index = self.data_buffer.find(data_end, head_index + len(data_head))
            if end_index == -1:
                 # 没找到完整帧尾，检查是否有不完整的帧尾
                partial_end = self.data_buffer.find("fffcff", search_start)
                if partial_end != -1:
                # 发现不完整帧尾，可能数据被截断，保留从下一个可能的帧头开始
                    next_head = self.data_buffer.find(data_head, partial_end)
                    if next_head != -1:
                        print(f"发现不完整帧尾，跳到下一个帧头: {next_head}")
                        self.data_buffer = self.data_buffer[next_head:]
                        continue

                # 保留从当前帧头开始的数据
                self.data_buffer = self.data_buffer[head_index:]
                break

            # 计算帧尾结束位置
            end_index += len(data_end)
            # 提取完整帧
            frame  = self.data_buffer[head_index:end_index]
             #验证帧的最小长度
            if len(frame) >= 22:  #最小帧长度检查 (帧头2+命令2+控制2+画面4+控件4+帧尾8)
                if self.validate_frame_structure(frame):
                    frames.append(frame)
                    # print(f"解析到有效帧: {frame}")
                else:
                    print(f"帧结构验证失败，跳过: {frame}")
            else:
                print(f"帧长度不足，跳过: {frame}")
            # 更新缓冲区
            self.data_buffer = self.data_buffer[end_index:]
            # 限制缓冲区大小，防止内存泄漏
            if len(self.data_buffer) > 2000:
                print("缓冲区过大，清空")
                self.data_buffer = ""
            # 返回所有帧的列表，而不是只返回最后一帧
        return frames if frames else None

    # 发送指令到设备
    def send_serial_data(self,serial_port, cmd_type,control_type=None,value=None,screen_id='0000',control_id ='0012'):
        """
        发送串口数据。
        参数:
        serial_port (serial.Serial): 初始化后的串口对象。
        cmd_type (str): 指令码 (例如 'B111')。
        control_type (str): 控件类型 (例如 '10,11传入传出')。
        value (str/int): 参数 (例如 '00000012113700' 或整数)。
        """
        try:
            # 帧头
            frame_header = b'\xEE'
            if cmd_type == 'b111' and control_type == '11':  # 文本框控件
                    # 处理不同类型的value参数
                if isinstance(value, int):
                    # 如果是整数，转换为字符串
                    text_value = str(value)
                elif isinstance(value, str):
                    # 如果已经是字符串，直接使用
                    text_value = value
                else:
                    return False
                
                text_value_hex = text_value.encode('ascii').hex() # 将文本值按ascii码再转换为十六进制
                end_marker_hex = '00'
                if screen_id == '0000' and control_id in ('0012', '0013','0014'):
                    params_upload= (bytes.fromhex(cmd_type)+ 
                                    bytes.fromhex(screen_id)+ 
                                    bytes.fromhex(control_id)+ 
                                    bytes.fromhex(control_type )+
                                    bytes.fromhex(text_value_hex)+ 
                                    bytes.fromhex(end_marker_hex))

            elif cmd_type == 'b111' and control_type == '10':  # 按钮控件
                pass
            elif cmd_type == 'f7':  # 发送RTC
                 params_upload = (bytes.fromhex(cmd_type) + bytes.fromhex(value))
            elif cmd_type in ('dd', 'e7', 'e8', 'e9'):  # 发送获取设备RTC/取消连接/下载/实时数据
                params_upload = (bytes.fromhex(cmd_type))

            else:
                print(f"未知的命令类型: {cmd_type}")
                return False
            # 帧尾
            frame_tail = b'\xFF\xFC\xFF\xFF'
            # 完整数据包
            data_packet = (frame_header + params_upload + frame_tail)
            # 发送数据
            if serial_port is not None:
                serial_port.write(data_packet)
                # print(f"发送数据: {data_packet.hex()}")
                return True
            else:
                print("串口未初始化。")
                return False
        except ValueError as e:
            print(f"发送错误： - {e}")
            return False
 
   # TF卡数据处理
    def feed_data(self, data_bytes: bytes):
        """
        把串口收到的data_bytes数据添加到缓冲区，并尝试拆包处理
        """
        self.recv_buffer.extend(data_bytes)
        # 循环找到所有完整帧
        while True:
            frame = self._extract_frame()
            if frame is None:
                break
            self._process_frame(frame)
        
    # 提取TF卡数据帧
    def _extract_frame(self):
        """
        从缓冲区中提取完整的帧，返回该帧字节数组或None
        帧格式假设：
         2字节帧头: 0xAA55
         1字节帧类型
         2字节序号(小端)
         2字节数据长度N(小端) 
         N字节数据
         2字节帧尾: 0xDFAF
        """
        buf = self.recv_buffer
        min_len = HEADER_LEN + 1 + 2 + 2 +2 # 帧头(2)+帧类型(1)+序号(2)+长度(2)+ 帧尾2
        if len(buf) < min_len:
            return None
        # 查找帧头
        idx = buf.find(FRAME_HEADER)
        if idx < 0:
            # 未找到帧头，全部丢弃
            # print(f"[调试] 未找到帧头，丢弃缓存数据: {buf.hex()}")
            # self.recv_buffer.clear()
            del buf[0]
            return None
        if idx > 0:
            # 丢弃帧头前无效数据
            # print(f"[调试] 丢弃帧头前无效字节: {buf[:idx].hex()}")
            del buf[:idx]
           
        if len(buf) < min_len:
            return None
        frame_type = buf[HEADER_LEN]
        seq = struct.unpack('<H', buf[HEADER_LEN+1:HEADER_LEN+3])[0]
        data_len = struct.unpack('<H', buf[HEADER_LEN+3:HEADER_LEN+5])[0]

        full_len = HEADER_LEN + 1 + 2 + 2 + data_len +2
        if len(buf) < full_len:
            # 数据不够完整，等待下一次接收
            return None
         # 检查帧尾是否为DF AF
        # tail = buf[full_len - 2 : full_len]
        # if tail != FRAME_TAIL:
        #     print(f"[错误] 帧尾不匹配: 实际={tail.hex()}，期望={FRAME_TAIL.hex()}，丢1字节继续")
        # # #     # 可能帧头错位，丢弃当前帧头这2字节，尝试下一个
        #     del buf[0]
        #     return None
        
        # 完整帧输出
        frame_data = buf[:full_len]
        del buf[:full_len]

        # print(f"[调试] 收到完整帧: type={frame_type}, seq={seq}, length={data_len}, data={frame[7:7+data_len].hex()}")
        return frame_data
    #处理TF卡数据帧
    def _process_frame(self, frame_bytes: bytes):
        frame_type = frame_bytes[2]
        seq = struct.unpack('<H', frame_bytes[3:5])[0]
        data_len = struct.unpack('<H', frame_bytes[5:7])[0]
        data = frame_bytes[7:7+data_len]

        if frame_type == START_FRAME:
            # 假设data中是4字节uint32预期行数
            if data_len >=4:
                self.expected_lines = struct.unpack('<I', data[:4])[0]
            else:
                self.expected_lines = 0
            # 重置计数和去重集合
            self.received_lines = 0
            self.received_seq_set.clear()
            self.content_list.clear()     # 重置内容缓存
            print(f"[开始帧] seq={seq} 预期接收行数={self.expected_lines}")

        elif frame_type == DATA_FRAME:
            # 去重判断
            if seq in self.received_seq_set:
                print(f"[重复数据帧] seq={seq} 丢弃")
                return
            self.received_seq_set.add(seq)
            self.received_lines += 1
            # 解析数据内容（根据具体内容改）
            try:
                content = data.decode('utf-8').strip()
                self.content_list.append(content)
            except Exception as e:
                print('tf内容解码失败')
                content = ''
            # print(f"[数据帧] seq={seq} 内容={content}")
            progress = int(self.received_lines /self.expected_lines *100)
            progress_item = QTableWidgetItem(f"{progress}%")
            self.dl_dialog.tableWidget.setItem(self.click_row, 2, progress_item)
            
        elif frame_type == END_FRAME:
            print(f"[结束帧] seq={seq} 实际接收数据行数={self.received_lines}, 预期={self.expected_lines}")
            if self.received_lines != self.expected_lines:
                print("[警告] 接收数据行数与预期不符，可能存在丢帧")

            try:
                file_path = os.path.join(self.script_dir,self.download_file_name)
                with open(file_path , 'w', newline='', encoding='utf-8') as f:
                    writer = csv.writer(f)
                    for content in self.content_list:
                        row = content.split(',')
                        writer.writerow(row)  # 注意加列表包裹，写入一列数据
                print(f"[写文件完成] 共写入{len(self.content_list)}行")
                self.dl_dialog.tableWidget.setEnabled(True)  # 启用项，使其可点击/交互
            except Exception as e:
                print("写入CSV文件失败:", e)

        else:
            print(f"[错误] 未知帧类型 {frame_type}，seq={seq}")


    
